//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by FernFlower decompiler)
//

package com.easyaop.api.support;

import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodType;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import org.objectweb.asm.ConstantDynamic;
import org.objectweb.asm.Handle;
import org.objectweb.asm.Label;
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Type;

import com.easyaop.commons.ASMOpcodes;
import com.easyaop.commons.Constants;

public class AnalyzerAdapter extends MethodVisitor {
    public List<Object> locals;

    public List<Object> stack;

    private List<Label> labels;

    public Map<Object, Object> uninitializedTypes;

    private String owner;

    public AnalyzerAdapter(String owner, int access, String name, String descriptor, MethodVisitor methodVisitor) {
        this(ASMOpcodes.ASM_API, owner, access, name, descriptor, methodVisitor);
        if (this.getClass() != AnalyzerAdapter.class) {
            throw new IllegalStateException();
        }
    }

    protected AnalyzerAdapter(int api, String owner, int access, String name, String descriptor,
                              MethodVisitor methodVisitor) {
        super(api, methodVisitor);
        this.owner = owner;
        this.locals = new ArrayList<>();
        this.stack = new ArrayList<>();
        this.uninitializedTypes = new HashMap<>();

        if ((access & ASMOpcodes.ACC_STATIC) == 0) {
            if (Constants.NAME_CTOR.equals(name)) {
                this.locals.add(ASMOpcodes.UNINITIALIZED_THIS);
            } else {
                this.locals.add(owner);
            }
        }

        Type[] argumentTypes = Type.getArgumentTypes(descriptor);
        for (Type argType : argumentTypes) {
            addArgumentTypeToLocals(argType);
        }
    }

    private void addArgumentTypeToLocals(Type argType) {
        switch (argType.getSort()) {
            case Type.BOOLEAN:
            case Type.CHAR:
            case Type.BYTE:
            case Type.SHORT:
            case Type.INT:
                this.locals.add(ASMOpcodes.INTEGER);
                break;
            case Type.FLOAT:
                this.locals.add(ASMOpcodes.FLOAT);
                break;
            case Type.LONG:
                this.locals.add(ASMOpcodes.LONG);
                this.locals.add(ASMOpcodes.TOP);
                break;
            case Type.DOUBLE:
                this.locals.add(ASMOpcodes.DOUBLE);
                this.locals.add(ASMOpcodes.TOP);
                break;
            case Type.ARRAY:
                this.locals.add(argType.getDescriptor());
                break;
            case Type.OBJECT:
                this.locals.add(argType.getInternalName());
                break;
            default:
                throw new AssertionError();
        }
    }

    public void visitFrame(int type, int numLocal, Object[] local, int numStack, Object[] stack) {
        if (type != -1) {
            throw new IllegalArgumentException("AnalyzerAdapter only accepts expanded frames (see ClassReader" +
                    ".EXPAND_FRAMES)");
        }
        super.visitFrame(type, numLocal, local, numStack, stack);
        resetLocalsAndStack();
        visitFrameTypes(numLocal, local, this.locals);
        visitFrameTypes(numStack, stack, this.stack);
    }

    private void resetLocalsAndStack() {
        if (this.locals != null) {
            this.locals.clear();
            this.stack.clear();
        } else {
            this.locals = new ArrayList<>();
            this.stack = new ArrayList<>();
        }
    }

    private static void visitFrameTypes(int numTypes, Object[] frameTypes, List<Object> result) {
        for (int i = 0; i < numTypes; i++) {
            Object frameType = frameTypes[i];
            result.add(frameType);
            if (frameType == ASMOpcodes.LONG || frameType == ASMOpcodes.DOUBLE) {
                result.add(ASMOpcodes.TOP);
            }
        }
    }

    public void visitInsn(int opcode) {
        super.visitInsn(opcode);
        executeOpcode(opcode, 0, null);
        if (isTerminatingOpcode(opcode)) {
            this.locals = null;
            this.stack = null;
        }
    }

    private boolean isTerminatingOpcode(int opcode) {
        return (opcode >= ASMOpcodes.IRETURN && opcode <= ASMOpcodes.RETURN) || opcode == ASMOpcodes.ATHROW;
    }

    public void visitIntInsn(int opcode, int operand) {
        super.visitIntInsn(opcode, operand);
        executeOpcode(opcode, operand, null);
    }

    public void visitVarInsn(int opcode, int varIndex) {
        super.visitVarInsn(opcode, varIndex);
        executeOpcode(opcode, varIndex, null);
    }

    public void visitTypeInsn(int opcode, String type) {
        if (opcode == ASMOpcodes.NEW) {
            if (this.labels == null) {
                Label label = new Label();
                this.labels = new ArrayList<>(3);
                this.labels.add(label);
                if (this.mv != null) {
                    this.mv.visitLabel(label);
                }
            }
            for (Label label : this.labels) {
                this.uninitializedTypes.put(label, type);
            }
        }
        super.visitTypeInsn(opcode, type);
        executeOpcode(opcode, 0, type);
    }

    public void visitFieldInsn(int opcode, String owner, String name, String descriptor) {
        super.visitFieldInsn(opcode, owner, name, descriptor);
        executeOpcode(opcode, 0, descriptor);
    }

    public void visitMethodInsn(int opcodeAndSource, String owner, String name, String descriptor,
                                boolean isInterface) {
        super.visitMethodInsn(opcodeAndSource, owner, name, descriptor, isInterface);
        if (this.locals != null) {
            handleMethodCall(opcodeAndSource & 0xFFFFFEFF, descriptor, name);
        } else {
            this.labels = null;
        }
    }

    private void handleMethodCall(int opcode, String descriptor, String name) {
        pop(descriptor);
        if (opcode != ASMOpcodes.INVOKESTATIC) {
            Object value = pop();
            if (opcode == ASMOpcodes.INVOKESPECIAL && Constants.NAME_CTOR.equals(name)) {
                Object initializedValue = ASMOpcodes.UNINITIALIZED_THIS.equals(value) ? this.owner :
                        this.uninitializedTypes.get(value);
                updateLocalsAndStack(value, initializedValue);
            }
        }
        pushDescriptor(descriptor);
        this.labels = null;
    }

    private void updateLocalsAndStack(Object oldValue, Object newValue) {
        for (int i = 0; i < this.locals.size(); i++) {
            if (this.locals.get(i) == oldValue) {
                this.locals.set(i, newValue);
            }
        }
        for (int i = 0; i < this.stack.size(); i++) {
            if (this.stack.get(i) == oldValue) {
                this.stack.set(i, newValue);
            }
        }
    }

    public void visitInvokeDynamicInsn(String name, String descriptor, Handle bootstrapMethodHandle,
                                       Object... bootstrapMethodArguments) {
        super.visitInvokeDynamicInsn(name, descriptor, bootstrapMethodHandle, bootstrapMethodArguments);
        if (this.locals != null) {
            pop(descriptor);
            pushDescriptor(descriptor);
            this.labels = null;
        }
    }

    public void visitJumpInsn(int opcode, Label label) {
        super.visitJumpInsn(opcode, label);
        executeOpcode(opcode, 0, null);
        if (opcode == ASMOpcodes.GOTO) {
            this.locals = null;
            this.stack = null;
        }
    }

    public void visitLabel(Label label) {
        super.visitLabel(label);
        if (this.labels == null) {
            this.labels = new ArrayList<>(3);
        }
        this.labels.add(label);
    }

    public void visitLdcInsn(Object value) {
        super.visitLdcInsn(value);
        if (this.locals != null) {
            handleLdcValue(value);
            this.labels = null;
        }
    }

    private void handleLdcValue(Object value) {
        if (value instanceof Integer) {
            push(ASMOpcodes.INTEGER);
        } else if (value instanceof Long) {
            push(ASMOpcodes.LONG);
            push(ASMOpcodes.TOP);
        } else if (value instanceof Float) {
            push(ASMOpcodes.FLOAT);
        } else if (value instanceof Double) {
            push(ASMOpcodes.DOUBLE);
            push(ASMOpcodes.TOP);
        } else if (value instanceof String) {
            push(String.class);
        } else if (value instanceof Type) {
            int sort = ((Type) value).getSort();
            if (sort != Type.OBJECT && sort != Type.ARRAY) {
                if (sort != Type.METHOD) {
                    throw new IllegalArgumentException();
                }
                push(MethodType.class);
            } else {
                push(Class.class);
            }
        } else if (value instanceof Handle) {
            push(MethodHandle.class);
        } else if (value instanceof ConstantDynamic) {
            pushDescriptor(((ConstantDynamic) value).getDescriptor());
        } else {
            throw new IllegalArgumentException();
        }
    }

    public void visitIincInsn(int varIndex, int increment) {
        super.visitIincInsn(varIndex, increment);
        executeOpcode(ASMOpcodes.IINC, varIndex, null);
    }

    public void visitTableSwitchInsn(int min, int max, Label dflt, Label... labels) {
        super.visitTableSwitchInsn(min, max, dflt, labels);
        executeOpcode(ASMOpcodes.TABLESWITCH, 0, null);
        this.locals = null;
        this.stack = null;
    }

    public void visitLookupSwitchInsn(Label dflt, int[] keys, Label[] labels) {
        super.visitLookupSwitchInsn(dflt, keys, labels);
        executeOpcode(ASMOpcodes.LOOKUPSWITCH, 0, null);
        this.locals = null;
        this.stack = null;
    }

    public void visitMultiANewArrayInsn(String descriptor, int numDimensions) {
        super.visitMultiANewArrayInsn(descriptor, numDimensions);
        executeOpcode(ASMOpcodes.MULTIANEWARRAY, numDimensions, descriptor);
    }

    private Object get(int local) {
        return local < this.locals.size() ? this.locals.get(local) : ASMOpcodes.TOP;
    }

    private void set(int local, Object type) {
        while (local >= this.locals.size()) {
            this.locals.add(ASMOpcodes.TOP);
        }
        this.locals.set(local, type);
    }

    protected void push(Object type) {
        this.stack.add(type);
    }

    protected void pushArgumentTypes(Type... pTypes) {
        for (Type argumentType : pTypes) {
            pushType(argumentType);
        }
    }

    protected void pushArgumentTypes(String methodDescriptor) {
        pushArgumentTypes(Type.getArgumentTypes(methodDescriptor));
    }

    protected void pushType(Type type) {
        switch (type.getSort()) {
            case Type.BYTE:
            case Type.CHAR:
            case Type.INT:
            case Type.SHORT:
            case Type.BOOLEAN:
                push(ASMOpcodes.INTEGER);
                return;
            case Type.DOUBLE:
                push(ASMOpcodes.DOUBLE);
                push(ASMOpcodes.TOP);
                return;
            case Type.FLOAT:
                push(ASMOpcodes.FLOAT);
                return;
            case Type.LONG:
                push(ASMOpcodes.LONG);
                push(ASMOpcodes.TOP);
                return;
            case Type.OBJECT:
                push(type.getInternalName());
                break;
            case Type.VOID:
                return;
            case Type.ARRAY:
                push(type.getDescriptor());
                break;
            default:
                throw new AssertionError();
        }
    }

    protected void push(Class<?> type) {
        this.stack.add(Type.getType(type).getInternalName());
    }

    protected void pushDescriptor(String fieldOrMethodDescriptor) {
        Type type = fieldOrMethodDescriptor.charAt(0) == '(' ?
                Type.getReturnType(fieldOrMethodDescriptor) : Type.getType(fieldOrMethodDescriptor);
        pushType(type);
    }

    protected Object peek() {
        return this.stack.get(this.stack.size() - 1);
    }

    protected Object pop() {
        return this.stack.remove(this.stack.size() - 1);
    }

    protected void pop(int numSlots) {
        int size = this.stack.size();
        int end = size - numSlots;
        if (size > end) {
            this.stack.subList(end, size).clear();
        }
    }

    protected void pop(String descriptor) {
        char firstDescriptorChar = descriptor.charAt(0);
        if (firstDescriptorChar == '(') {
            int numSlots = 0;
            Type[] types = Type.getArgumentTypes(descriptor);
            for (Type type : types) {
                numSlots += type.getSize();
            }
            pop(numSlots);
        } else if (firstDescriptorChar != 'J' && firstDescriptorChar != 'D') {
            pop();
        } else {
            pop(2);
        }
    }

    private void executeOpcode(int opcode, int intArg, String stringArg) {
        if (opcode == ASMOpcodes.JSR || opcode == ASMOpcodes.RET) { // 替换 168/169 为 JSR/RET
            throw new IllegalArgumentException("JSR/RET are not supported");
        }
        if (this.locals == null) {
            this.labels = null;
            return;
        }
        Object value1;
        Object value2;
        Object value3;
        label89:
        switch (opcode) {
            case ASMOpcodes.NOP:
            case ASMOpcodes.INEG:
            case ASMOpcodes.LNEG:
            case ASMOpcodes.FNEG:
            case ASMOpcodes.DNEG:
            case ASMOpcodes.I2B:
            case ASMOpcodes.I2C:
            case ASMOpcodes.I2S:
            case ASMOpcodes.GOTO:
            case ASMOpcodes.RETURN:
                break;
            case ASMOpcodes.ACONST_NULL:
                this.push(ASMOpcodes.NULL);
                break;
            case ASMOpcodes.ICONST_M1:
            case ASMOpcodes.ICONST_0:
            case ASMOpcodes.ICONST_1:
            case ASMOpcodes.ICONST_2:
            case ASMOpcodes.ICONST_3:
            case ASMOpcodes.ICONST_4:
            case ASMOpcodes.ICONST_5:
            case ASMOpcodes.BIPUSH:
            case ASMOpcodes.SIPUSH:
                this.push(ASMOpcodes.INTEGER);
                break;
            case ASMOpcodes.LCONST_0:
            case ASMOpcodes.LCONST_1:
                this.push(ASMOpcodes.LONG);
                this.push(ASMOpcodes.TOP);
                break;
            case ASMOpcodes.FCONST_0:
            case ASMOpcodes.FCONST_1:
            case ASMOpcodes.FCONST_2:
                this.push(ASMOpcodes.FLOAT);
                break;
            case ASMOpcodes.DCONST_0:
            case ASMOpcodes.DCONST_1:
                this.push(ASMOpcodes.DOUBLE);
                this.push(ASMOpcodes.TOP);
                break;
            case ASMOpcodes.ILOAD:
            case ASMOpcodes.FLOAD:
            case ASMOpcodes.ALOAD:
                this.push(this.get(intArg));
                break;
            case ASMOpcodes.LLOAD:
            case ASMOpcodes.DLOAD:
                this.push(this.get(intArg));
                this.push(ASMOpcodes.TOP);
                break;

            case ASMOpcodes.IALOAD:
            case ASMOpcodes.BALOAD:
            case ASMOpcodes.CALOAD:
            case ASMOpcodes.SALOAD:
            case ASMOpcodes.IADD:
            case ASMOpcodes.ISUB:
            case ASMOpcodes.IMUL:
            case ASMOpcodes.IDIV:
            case ASMOpcodes.IREM:
            case ASMOpcodes.ISHL:
            case ASMOpcodes.ISHR:
            case ASMOpcodes.IUSHR:
            case ASMOpcodes.IAND:
            case ASMOpcodes.IOR:
            case ASMOpcodes.IXOR:
            case ASMOpcodes.L2I:
            case ASMOpcodes.D2I:
            case ASMOpcodes.FCMPL:
            case ASMOpcodes.FCMPG:
                this.pop(2);
                this.push(ASMOpcodes.INTEGER);
                break;
            case ASMOpcodes.LALOAD:
            case ASMOpcodes.D2L:
                this.pop(2);
                this.push(ASMOpcodes.LONG);
                this.push(ASMOpcodes.TOP);
                break;
            case ASMOpcodes.FALOAD:
            case ASMOpcodes.FADD:
            case ASMOpcodes.FSUB:
            case ASMOpcodes.FMUL:
            case ASMOpcodes.FDIV:
            case ASMOpcodes.FREM:
            case ASMOpcodes.L2F:
            case ASMOpcodes.D2F:
                this.pop(2);
                this.push(ASMOpcodes.FLOAT);
                break;
            case ASMOpcodes.DALOAD:
            case ASMOpcodes.L2D:
                this.pop(2);
                this.push(ASMOpcodes.DOUBLE);
                this.push(ASMOpcodes.TOP);
                break;

            case ASMOpcodes.AALOAD:
                this.pop(1);
                value1 = this.pop();
                if (value1 instanceof String) {
                    this.pushDescriptor(((String) value1).substring(1));
                } else if (value1 == ASMOpcodes.NULL) {
                    this.push(value1);
                } else {
                    this.push("java/lang/Object");
                }
                break;
            case ASMOpcodes.ISTORE:
            case ASMOpcodes.FSTORE:
            case ASMOpcodes.ASTORE:
                value1 = this.pop();
                set(intArg, value1);
                handleLongOrDouble(intArg);
                break;
            case ASMOpcodes.LSTORE:
            case ASMOpcodes.DSTORE:
                pop(1);
                value1 = this.pop();
                set(intArg, value1);
                set(intArg + 1, ASMOpcodes.TOP);
                handleLongOrDouble(intArg);
                break;
            case ASMOpcodes.IASTORE:
            case ASMOpcodes.FASTORE:
            case ASMOpcodes.AASTORE:
            case ASMOpcodes.BASTORE:
            case ASMOpcodes.CASTORE:
            case ASMOpcodes.SASTORE:
                this.pop(3);
                break;
            case ASMOpcodes.LASTORE:
            case ASMOpcodes.DASTORE:
                this.pop(4);
                break;
            case ASMOpcodes.POP:
            case ASMOpcodes.IFEQ:
            case ASMOpcodes.IFNE:
            case ASMOpcodes.IFLT:
            case ASMOpcodes.IFGE:
            case ASMOpcodes.IFGT:
            case ASMOpcodes.IFLE:
            case ASMOpcodes.TABLESWITCH:
            case ASMOpcodes.LOOKUPSWITCH:
            case ASMOpcodes.IRETURN:
            case ASMOpcodes.FRETURN:
            case ASMOpcodes.ARETURN:
            case ASMOpcodes.ATHROW:
            case ASMOpcodes.MONITORENTER:
            case ASMOpcodes.MONITOREXIT:
            case ASMOpcodes.IFNULL:
            case ASMOpcodes.IFNONNULL:
                this.pop(1);
                break;
            case ASMOpcodes.POP2:
            case ASMOpcodes.IF_ICMPEQ:
            case ASMOpcodes.IF_ICMPNE:
            case ASMOpcodes.IF_ICMPLT:
            case ASMOpcodes.IF_ICMPGE:
            case ASMOpcodes.IF_ICMPGT:
            case ASMOpcodes.IF_ICMPLE:
            case ASMOpcodes.IF_ACMPEQ:
            case ASMOpcodes.IF_ACMPNE:
            case ASMOpcodes.LRETURN:
            case ASMOpcodes.DRETURN:
                this.pop(2);
                break;
            case ASMOpcodes.DUP:
                value1 = this.pop();
                this.push(value1);
                this.push(value1);
                break;
            case ASMOpcodes.DUP_X1:
                value1 = this.pop();
                value2 = this.pop();
                this.push(value1);
                this.push(value2);
                this.push(value1);
                break;
            case ASMOpcodes.DUP_X2:
                value1 = this.pop();
                value2 = this.pop();
                value3 = this.pop();
                this.push(value1);
                this.push(value3);
                this.push(value2);
                this.push(value1);
                break;
            case ASMOpcodes.DUP2:
                value1 = this.pop();
                value2 = this.pop();
                this.push(value2);
                this.push(value1);
                this.push(value2);
                this.push(value1);
                break;
            case ASMOpcodes.DUP2_X1:
                value1 = this.pop();
                value2 = this.pop();
                value3 = this.pop();
                this.push(value2);
                this.push(value1);
                this.push(value3);
                this.push(value2);
                this.push(value1);
                break;
            case ASMOpcodes.DUP2_X2:
                value1 = this.pop();
                value2 = this.pop();
                value3 = this.pop();
                Object t4 = this.pop();
                this.push(value2);
                this.push(value1);
                this.push(t4);
                this.push(value3);
                this.push(value2);
                this.push(value1);
                break;
            case ASMOpcodes.SWAP:
                value1 = this.pop();
                value2 = this.pop();
                this.push(value1);
                this.push(value2);
                break;
            case ASMOpcodes.LADD:
            case ASMOpcodes.LSUB:
            case ASMOpcodes.LMUL:
            case ASMOpcodes.LDIV:
            case ASMOpcodes.LREM:
            case ASMOpcodes.LAND:
            case ASMOpcodes.LOR:
            case ASMOpcodes.LXOR:
                this.pop(4);
                this.push(ASMOpcodes.LONG);
                this.push(ASMOpcodes.TOP);
                break;

            case ASMOpcodes.DADD:
            case ASMOpcodes.DSUB:
            case ASMOpcodes.DMUL:
            case ASMOpcodes.DDIV:
            case ASMOpcodes.DREM:
                this.pop(4);
                this.push(ASMOpcodes.DOUBLE);
                this.push(ASMOpcodes.TOP);
                break;
            case ASMOpcodes.LSHL:
            case ASMOpcodes.LSHR:
            case ASMOpcodes.LUSHR:
                this.pop(3);
                this.push(ASMOpcodes.LONG);
                this.push(ASMOpcodes.TOP);
                break;

            case ASMOpcodes.IINC:
                set(intArg, ASMOpcodes.INTEGER);
                break;
            case ASMOpcodes.I2L:
            case ASMOpcodes.F2L:
                this.pop(1);
                this.push(ASMOpcodes.LONG);
                this.push(ASMOpcodes.TOP);
                break;
            case ASMOpcodes.I2F:
                this.pop(1);
                this.push(ASMOpcodes.FLOAT);
                break;
            case ASMOpcodes.I2D:
            case ASMOpcodes.F2D:
                this.pop(1);
                this.push(ASMOpcodes.DOUBLE);
                this.push(ASMOpcodes.TOP);
                break;
            case ASMOpcodes.F2I:
            case ASMOpcodes.ARRAYLENGTH:
            case ASMOpcodes.INSTANCEOF:
                this.pop(1);
                this.push(ASMOpcodes.INTEGER);
                break;
            case ASMOpcodes.LCMP:
            case ASMOpcodes.DCMPL:
            case ASMOpcodes.DCMPG:
                this.pop(4);
                this.push(ASMOpcodes.INTEGER);
                break;

            case ASMOpcodes.GETSTATIC:
                pushDescriptor(stringArg);
                break;
            case ASMOpcodes.PUTSTATIC:
                pop(stringArg);
                break;
            case ASMOpcodes.GETFIELD:
                this.pop(1);
                this.pushDescriptor(stringArg);
                break;
            case ASMOpcodes.PUTFIELD:
                this.pop(stringArg);
                this.pop();
                break;
            case ASMOpcodes.NEW:
                this.push(this.labels.get(0));
                break;
            case ASMOpcodes.NEWARRAY:
                pop();
                handleNewArray(intArg);
                break;
            case ASMOpcodes.ANEWARRAY:
                pop();
                pushDescriptor("[" + Type.getObjectType(stringArg));
                break;

            case ASMOpcodes.CHECKCAST:
                pop();
                pushDescriptor(Type.getObjectType(stringArg).getDescriptor());
                break;
            case ASMOpcodes.MULTIANEWARRAY:
                pop(intArg);
                pushDescriptor(stringArg);
            default:
                throw new IllegalArgumentException("Invalid opcode " + opcode);
        }

        this.labels = null;
    }

    private void handleLongOrDouble(int intArg) {
        if (intArg > 0) {
            Object value = get(intArg - 1);
            if (value == ASMOpcodes.LONG || value == ASMOpcodes.DOUBLE) {
                set(intArg - 1, ASMOpcodes.TOP);
            }
        }
    }

    private void handleNewArray(int intArg) {
        switch (intArg) {
            case 4:
                pushDescriptor("[Z");
                break;
            case 5:
                pushDescriptor("[C");
                break;
            case 6:
                pushDescriptor("[F");
                break;
            case 7:
                pushDescriptor("[D");
                break;
            case 8:
                pushDescriptor("[B");
                break;
            case 9:
                pushDescriptor("[S");
                break;
            case 10:
                pushDescriptor("[I");
                break;
            case 11:
                pushDescriptor("[J");
                break;
            default:
                throw new IllegalArgumentException("Invalid array type " + intArg);
        }
    }
}